package com.simpou.commons.web.helper;

import com.simpou.commons.utils.lang.Conditionals;
import com.simpou.commons.utils.lang.RefHashMap;
import com.simpou.commons.utils.reflection.Annotations;
import com.simpou.commons.utils.reflection.Generics;
import com.simpou.commons.utils.reflection.Reflections;
import com.simpou.commons.utils.reflection.condition.BasicField;
import com.simpou.commons.web.model.MultivaluedMapImpl;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * @author jonas1
 * @version 23/09/13
 * @since 23/09/13
 */
public class PojoToFormParamsConversor extends AbstractPojoAndFormParamsConversor<Object, Map<String, List<String>>> {

    private static final String XML_ANNOT_DEF_VALUE = "##default";

    public PojoToFormParamsConversor() {
        this(null);
    }

    public PojoToFormParamsConversor(final ConversorContext context) {
        super(context);
    }

    public static void execute(final Object object, final Map<String, List<String>> multiMap, final ConversorContext context) throws Exception {
        execute(object, multiMap, null, context, new RefHashMap<Object, Set<Field>>());
    }

    private static void execute(final Object object,
                                final Map<String, List<String>> multiMap,
                                final String preffix,
                                final ConversorContext context,
                                final RefHashMap<Object, Set<Field>> visiteds) throws Exception {
        final List<Field> fields = getFields(object.getClass(), context);
        execute(fields, object, multiMap, preffix, context, visiteds);
    }

    private static final Comparator<Field> fieldComparator = new Comparator<Field>() {
        @Override
        public int compare(final Field o1, final Field o2) {
            return o1.getName().equals(o2.getName())
                    && o1.getDeclaringClass() == o2.getDeclaringClass() ? 0 : 1;
        }
    };

    private static void execute(final List<Field> fields,
                                final Object object,
                                final Map<String, List<String>> multiMap,
                                final String preffix,
                                final ConversorContext context,
                                final RefHashMap<Object, Set<Field>> visiteds) throws Exception {
        final Class<? extends Object> objClass = object.getClass();
        final boolean fieldAccessPriority = objClass.isAnnotationPresent(XmlAccessorType.class)
                && objClass.getAnnotation(XmlAccessorType.class).value().equals(XmlAccessType.FIELD);

        final Set<Field> fieldsVisited;
        if (visiteds.containsKey(object)) {
            fieldsVisited = visiteds.get(object);
        } else {
            fieldsVisited = new HashSet<Field>();
            visiteds.putNew(object, fieldsVisited);
        }

        for (Field field : fields) {
            final String fieldName = field.getName();
            field.setAccessible(true);
            final Object value = field.get(object);
            if (value != null) {
                final Class<?> type = Reflections.getFieldType(objClass, field);
                final String name;
                if (preffix == null) {
                    name = fieldName;
                } else {
                    name = preffix + "[" + fieldName + "]";
                }
                if (BasicField.isBasic(type)) {
                    String finalName;
                    final XmlAttribute attAnnot = Annotations.getPojoFieldAnnotation(field, XmlAttribute.class, fieldAccessPriority);
                    if (attAnnot == null) {
                        finalName = name;
                    } else {
                        finalName = context.getAttPreffix();
                        final String annotName = attAnnot.name();
                        if (annotName.equals(XML_ANNOT_DEF_VALUE)) {
                            finalName += name;
                        } else {
                            finalName += annotName;
                        }
                    }
                    multiMap.put(finalName, Arrays.asList(context.marshal(field, value, fieldAccessPriority)));
                } else if (type.isArray()) {
                    final int arrLength = Array.getLength(value);
                    if (arrLength > 0) {
                        if (BasicField.isBasic(type.getComponentType())) {
                            final List<String> values = new ArrayList<String>();
                            for (int i = 0; i < arrLength; i++) {
                                values.add(Array.get(value, i).toString());
                            }
                            multiMap.put(name + "[]", values);
                        } else {
                            for (int i = 0; i < arrLength; i++) {
                                execute(
                                        Array.get(value, i),
                                        multiMap,
                                        name + "[" + i + "]",
                                        context,
                                        visiteds);
                            }
                        }
                    }
                } else if (Collection.class.isAssignableFrom(type)) {
                    final List<Class<?>> genTypes = Generics.getParameterizedTypes(field.getGenericType());
                    if (Conditionals.isEmpty(genTypes)) {
                        throw new UnsupportedOperationException("Raw collection fields is no supported. Field: " + field.toString());
                    } else {
                        final Class<?> genType = genTypes.get(0);
                        final Collection<?> collection = (Collection<?>) value;
                        final String finalName = getCollectionFieldName(field, name, fieldAccessPriority, context);
                        if (BasicField.isBasic(genType)) {
                            final List<String> values = new ArrayList<String>();
                            for (Object item : collection) {
                                values.add(item.toString());
                            }
                            multiMap.put(finalName + "[]", values);
                        } else {
                            int i = 0;
                            for (Object item : collection) {
                                execute(item, multiMap, finalName + "[" + i + "]", context, visiteds);
                                i++;
                            }
                        }
                    }
                } else {
                    if (fieldsVisited.contains(field)) {
                        //recursão detectada
                        return;
                    }
                    fieldsVisited.add(field);
                    execute(value, multiMap, name, context, visiteds);
                }
            }
        }
    }

    @Override
    public Map<String, List<String>> execute(final Object object) throws Exception {
        final Map<String, List<String>> multiMap = new MultivaluedMapImpl();
        PojoToFormParamsConversor.execute(object, multiMap, getContext());
        return multiMap;
    }

    @Override
    public void doOnError(final Object object, final Throwable throwable) {
        throw new RuntimeException(throwable);
    }

    private static String getCollectionFieldName(final Field field, final String name,
                                                 final boolean fieldAccessPriority,
                                                 final ConversorContext context) {
        String finalName;
        {
            final XmlElementWrapper annot = Annotations.getPojoFieldAnnotation(field, XmlElementWrapper.class, fieldAccessPriority);
            if (annot == null) {
                finalName = name;
            } else {
                final String annotName = annot.name();
                if (annotName.equals(XML_ANNOT_DEF_VALUE)) {
                    finalName = name;
                } else {
                    finalName = annotName;
                }
            }
        }
        {
            final Class<?> declaringClass = field.getDeclaringClass();
            final Map<Class<?>, Map<String, String>> wrappers = context.getWrappers();
            final boolean isParamTypeWrapped = wrappers != null &&
                    wrappers.containsKey(declaringClass) &&
                    wrappers.get(declaringClass).containsKey(field.getName());


            if (isParamTypeWrapped) {
                finalName += "[" + wrappers.get(declaringClass).get(field.getName()) + "]";
            } else {
                final XmlElement annot = Annotations.getPojoFieldAnnotation(field, XmlElement.class, false);
                if (annot != null) {
                    final String annotName = annot.name();
                    if (!annotName.equals(XML_ANNOT_DEF_VALUE)) {
                        finalName += "[" + annotName + "]";
                    }
                }
            }
        }

        return finalName;
    }
}
